/*
 *  Property Mapper, simple automatic GUI generator for JavaBeans using annotations.
 *  Copyright (C) 2008 Jon Lennersten, Johan Hedin
 *
 *  This file is part of Property Mapper.
 *
 *  Property Mapper is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Property Mapper is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Property Mapper.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mawell.common.propertymapper;

import java.util.logging.Level;
import java.util.logging.Logger;

import java.lang.reflect.Method;
import java.util.*;


/**
 * Class for mapping an object with get and set pairs to
 * something configurable in a GUI
 *
 * @version $Id: PropertyMapper.java,v 1.12 2007/04/24 13:42:23 jon Exp $
 */
public class PropertyMapper {

    protected static Logger logger = Logger.getLogger(PropertyMapper.class.getName());

    private Object description;

    /**
     * Get the object used for getting the descriptions
     *
     * @return The object used for getting the descriptions
     */
    public Object getDescription() {
        return description;
    }

    /**
     * Set the object used for getting the descriptions. The method is given in the
     * <code>descriptionMethodName</code> in the <code>Property</code> annotation
     *
     * @param description The object used for getting the descriptions
     * @see @Property
     */
    public void setDescription(Object description) {
        this.description = description;
    }

    /**
     * Gets the configuration of an object with level 0.
     *
     * @param o The object to get the configuration for
     * @return The configuration of the object
     * @throws com.mawell.common.propertymapper.PropertyMapperException
     *
     * @see #getConfiguration(Object, int)
     */
    public PropertyConfigurationContainer getConfiguration(Object o) throws PropertyMapperException {
        return getConfiguration(o, 0);
    }

    /**
     * Gets configuration of an object. The configuration can then be altered. The new
     * configuration is imposed on the object via <code>setProperties</code>. Only
     * the properties with the level lower or equal to the provided level will be shown.
     * <p/>
     * <p/>
     * In order to prepare an object for configuration, the object must be annontated as configurable.
     * Also each property exposed must be annotated as properties. An optional level of expertice can
     * be given. For each property, a method name can be given for the lookup of the property description.
     * Objects will be handled recursively.
     *
     * @param o     The object to obatin configuration of
     * @param level The level of expertice
     * @return The configuration of the object
     * @throws com.mawell.common.propertymapper.PropertyMapperException
     *
     * @see #setProperties(com.mawell.common.propertymapper.PropertyConfigurationContainer, Object)
     * @see @PropertyConfigurable
     * @see @Property
     * @see #setDescription(Object)
     */
    public PropertyConfigurationContainer getConfiguration(Object o, int level) throws PropertyMapperException {
        Class c = o.getClass();
        if(!c.isAnnotationPresent(PropertyConfigurable.class)) {
            throw new PropertyMapperException("Class " + c.getName() + " not annotated with "
                    + PropertyConfigurable.class.getName());
        }
        try {
            PropertyConfigurationContainer pcc = new PropertyConfigurationContainer();
            Set<String> properties = new HashSet<String>();
            for(Method m : c.getMethods()) {
                if(m.isAnnotationPresent(Property.class)
                        && m.getAnnotation(Property.class).level() <= level) {
                    Method get;
                    Method set;
                    String property = null;
                    Class type;
                    String possibleValues[] = null;
                    boolean allKeysMustBePresent = false;
                    KeyAndValues[] keys = null;
                    boolean allValuesMustBePresent = false;
                    KeyAndValues[] values = null;

                    String valueEntity = null;
                    String valueNameField = null;
                    String valueEnumClass = null;
                    String keyEntity = null;
                    String keyNameField = null;
                    String keyEnumClass = null;

                    if(m.getName().startsWith("get")) {
                        get = m;
                        property = m.getName().substring(3);
                        type = m.getReturnType();
                        set = c.getMethod("set" + property, type);
                    } else if(m.getName().startsWith("is")) {
                        get = m;
                        property = m.getName().substring(2);
                        type = m.getReturnType();
                        set = c.getMethod("set" + property, type);
                    } else if(m.getName().startsWith("set")) {
                        set = m;
                        property = m.getName().substring(3);
                        type = m.getParameterTypes()[0];
                        try {
                            get = c.getMethod("get" + property, new Class[0]);
                        } catch(NoSuchMethodException nsme) {
                            get = c.getMethod("is" + property, new Class[0]);
                        }
                    } else {
                        throw new PropertyMapperException("Method " + m.getName() + " not get or set");
                    }
                    if(m.isAnnotationPresent(PossibleValues.class)) {
                        possibleValues = m.getAnnotation(PossibleValues.class).possibleValues();
                    }

                    String descriptionMethod = null;
                    String descriptionString = null;
                    if(description != null && (descriptionMethod = m.getAnnotation(Property.class).descriptionMethodName()).length() != 0) {
                        try {
                            descriptionString = (String)description.getClass()
                                    .getMethod(descriptionMethod, null).invoke(description);
                        } catch(Exception e) {
                            logger.log(Level.WARNING, e.getMessage(), e);
                        }
                    }
                    if(descriptionString == null) {
                        descriptionString = m.getAnnotation(Property.class).description();
                    }
                    if(descriptionString == null || descriptionString.length() == 0) {
                        //Use the member name, replace capital letters with space+lower and capitalize first letter
                        StringBuffer sb = new StringBuffer();
                        sb.append(property.substring(0, 1).toUpperCase());
                        for(int i = 1; i < property.length(); i++) {

                            if(property.charAt(i) >= 'A' && property.charAt(i) <= 'Z') {
                                sb.append(" ").append(property.substring(i, i + 1).toLowerCase());
                            } else {
                                sb.append(property.charAt(i));
                            }
                        }
                        descriptionString = sb.toString();
                    }
                    if(m.isAnnotationPresent(FixedKeys.class)) {
                        FixedKeys a = m.getAnnotation(FixedKeys.class);
                        allKeysMustBePresent = a.allMustBePresent();
                        if(a.keysMethodName().length() != 0) {
                            keys = (KeyAndValues[])o.getClass().getMethod(a.keysMethodName(), null).invoke(o);
                        }
                    }
                    if(m.isAnnotationPresent(FixedValues.class)) {
                        FixedValues a = m.getAnnotation(FixedValues.class);
                        allValuesMustBePresent = a.allMustBePresent();
                        if(a.valuesMethodName().length() != 0) {
                            values = (KeyAndValues[])o.getClass().getMethod(a.valuesMethodName(), null).invoke(o);
                        }
                    }
                    if(m.isAnnotationPresent(ValueIsID.class)) {
                        ValueIsID a = m.getAnnotation(ValueIsID.class);
                        valueEntity = a.entity();
                        valueNameField = a.nameField();
                    }
                    if(m.isAnnotationPresent(ValueIsEnum.class)) {
                        ValueIsEnum a = m.getAnnotation(ValueIsEnum.class);
                        valueEnumClass = a.enumClass();
                    }
                    if(m.isAnnotationPresent(KeyIsID.class)) {
                        KeyIsID a = m.getAnnotation(KeyIsID.class);
                        keyEntity = a.entity();
                        keyNameField = a.nameField();
                    }
                    if(m.isAnnotationPresent(KeyIsEnum.class)) {
                        KeyIsEnum a = m.getAnnotation(KeyIsEnum.class);
                        keyEnumClass = a.enumClass();
                    }
                    PropertyConfiguration pc = null;
                    if(!properties.contains(property)) {
                        if(type.equals(int.class)) {
                            IntPropertyConfiguration ipc = new IntPropertyConfiguration();
                            ipc.setValue((Integer)get.invoke(o, null));
                            pc = ipc;
                        } else if(type.equals(String.class)) {
                            StringPropertyConfiguration spc = new StringPropertyConfiguration();
                            spc.setValue((String)get.invoke(o, null));
                            spc.setPossibleValues(possibleValues);
                            spc.setValueEntity(valueEntity);
                            spc.setValueNameField(valueNameField);
                            pc = spc;
                        } else if(type.equals(Date.class)) {
                            DatePropertyConfiguration dpc = new DatePropertyConfiguration();
                            dpc.setValue((Date)get.invoke(o, null));
                            pc = dpc;
                        } else if(type.equals(boolean.class)) {
                            BooleanPropertyConfiguration bpc = new BooleanPropertyConfiguration();
                            bpc.setValue((Boolean)get.invoke(o, null));
                            pc = bpc;
                        } else if(get.invoke(o, null) instanceof Map) {
                            MapPropertyConfiguration mpc = new MapPropertyConfiguration();
                            mpc.setValue((Map)get.invoke(o, null));
                            mpc.setValueEntity(valueEntity);
                            mpc.setValueNameField(valueNameField);
                            mpc.setValueEnumClass(valueEnumClass);
                            mpc.setKeyEntity(keyEntity);
                            mpc.setKeyNameField(keyNameField);
                            mpc.setKeyEnumClass(keyEnumClass);
                            pc = mpc;
                        } else if(get.invoke(o, null) instanceof List) {
                            ListPropertyConfiguration lpc = new ListPropertyConfiguration();
                            lpc.setValue((List)get.invoke(o, null));
                            lpc.setValueEntity(valueEntity);
                            lpc.setValueNameField(valueNameField);
                            lpc.setValueEnumClass(valueEnumClass);
                            pc = lpc;
                        } else if(type.getEnumConstants() != null) {
                            EnumPropertyConfiguration epc = new EnumPropertyConfiguration();
                            epc.setEnumClass(type);
                            epc.setValue((Enum)get.invoke(o, null));
                            epc.setPossibleValues(possibleValues);
                            pc = epc;
                        } else if(!type.isPrimitive()) {
                            PropertyConfigurationContainer pcl = getConfiguration(get.invoke(o, null), level);
                            pc = pcl;
                        }
                        if(pc == null) {
                            throw new PropertyMapperException("Method " + m.getName() + " has non configurable"
                                    + " type " + type);
                        }
                        pc.setProperty(property);
                        pc.setMandatory(m.getAnnotation(Property.class).mandatory());
                        pc.get = get;
                        pc.set = set;
                        pc.setDescription(descriptionString);
                        pcc.add(pc);
                        properties.add(property);
                    }
                }
            }
            return pcc;
        } catch(Exception e) {
            if(e instanceof PropertyMapperException) {
                throw (PropertyMapperException)e;
            } else {
                PropertyMapperException pme = new PropertyMapperException(e.getMessage(), e);
                throw pme;
            }
        }
    }

    /**
     * Imposed the new configuraion on an object. get and set pairs not annotated as properties
     * will be left alone.
     *
     * @param configuration The new configuration
     * @param o             The object to configure
     * @throws com.mawell.common.propertymapper.PropertyMapperException
     *
     */
    public void setProperties(PropertyConfigurationContainer configuration, Object o) throws PropertyMapperException {
        try {
            for(PropertyConfiguration pc : configuration.getPropertyConfigurations()) {
                if(pc instanceof IntPropertyConfiguration) {
                    IntPropertyConfiguration ipc = (IntPropertyConfiguration)pc;
                    ipc.set.invoke(o, ipc.getValue());
                }
                if(pc instanceof BooleanPropertyConfiguration) {
                    BooleanPropertyConfiguration bpc = (BooleanPropertyConfiguration)pc;
                    bpc.set.invoke(o, bpc.isValue());
                }
                if(pc instanceof StringPropertyConfiguration) {
                    StringPropertyConfiguration spc = (StringPropertyConfiguration)pc;
                    spc.set.invoke(o, spc.getValue());
                }
                if(pc instanceof DatePropertyConfiguration) {
                    DatePropertyConfiguration dpc = (DatePropertyConfiguration)pc;
                    dpc.set.invoke(o, dpc.getValue());
                }
                if(pc instanceof MapPropertyConfiguration) {
                    MapPropertyConfiguration mpc = (MapPropertyConfiguration)pc;
                    Object oldO = mpc.get.invoke(o, null);
                    Object newO = mpc.getValue();
                    if(!newO.getClass().isInstance(oldO)) {
                        PropertyMapperException pme = new PropertyMapperException("Map property changed from class " + oldO.getClass().getName()
                                + " to " + newO.getClass().getName());
                        throw pme;
                    }
                    mpc.set.invoke(o, mpc.getValue());
                }
                if(pc instanceof ListPropertyConfiguration) {
                    ListPropertyConfiguration lpc = (ListPropertyConfiguration)pc;
                    Object oldO = lpc.get.invoke(o, null);
                    Object newO = lpc.getValue();
                    if(!newO.getClass().isInstance(oldO)) {
                        PropertyMapperException pme = new PropertyMapperException("List property changed from class " + oldO.getClass().getName()
                                + " to " + newO.getClass().getName());
                        throw pme;
                    }
                    lpc.set.invoke(o, lpc.getValue());
                }
                if(pc instanceof EnumPropertyConfiguration) {
                    EnumPropertyConfiguration epc = (EnumPropertyConfiguration)pc;
                    epc.set.invoke(o, epc.getValue());
                }
                if(pc instanceof PropertyConfigurationContainer) {
                    PropertyConfigurationContainer pcc = (PropertyConfigurationContainer)pc;
                    setProperties(pcc, pcc.get.invoke(o, null));
                }
            }
        } catch(Exception e) {
            if(e instanceof PropertyMapperException) {
                throw (PropertyMapperException)e;
            } else {
                PropertyMapperException pme = new PropertyMapperException(e.getMessage(), e);
                throw pme;
            }
        }
    }
}